﻿// Project: Salient
// http://salient.codeplex.com
// 
// Copyright 2010, Sky Sanders <sky at skysanders.net>
// licensed under the GPL Version 2 licenses.
// http://salient.codeplex.com/license
// 
// 
// You may use this code under the terms of the GNU General Public 
// License (GPL) Version 2.
using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace Salient.Net
{
    /// <summary>
    ///   This is a thread-safe class that will cache requests for a configurable interval according to user definable rules.
    /// </summary>
    public class GatewayCache : IGatewayCache
    {
        private readonly Dictionary<string, CacheItem> _cache;
        private readonly object _cacheLock;
        private readonly Dictionary<string, TimeSpan> _rules;

        /// <summary>
        /// </summary>
        public GatewayCache()
        {
            _cacheLock = new object();
            _rules = new Dictionary<string, TimeSpan>();
            _cache = new Dictionary<string, CacheItem>();
            DefaultCacheDuration = new TimeSpan(0, 1, 0);
        }

        #region IGatewayCache Members

        /// <summary>
        ///   The default period of time to cache requests. Default is 1 minute.
        /// </summary>
        public TimeSpan DefaultCacheDuration { get; set; }



        /// <summary>
        ///   Removes a cached item
        /// </summary>
        /// <param name = "url"></param>
        public virtual void Remove(string url)
        {
            lock (_cacheLock)
            {
                if (_cache.ContainsKey(url))
                {
                    // trace item removed
                    _cache.Remove(url);
                }
            }
        }

        /// <summary>
        ///   Sets/Replaces an object in the cache and resets the scavenge time according to the rule specified.
        ///   If the rule does not exist, the default cache duration is applied
        /// </summary>
        /// <param name = "url"></param>
        /// <param name = "rule"></param>
        /// <param name = "data"></param>
        public virtual void Set(string url, string rule, object data)
        {


            lock (_cacheLock)
            {
                // scavenge will clear old items and check for this
                // one as well. 
                Scavenge(url);

                TimeSpan expiration = string.IsNullOrEmpty(rule)
                    ? DefaultCacheDuration
                    : (_rules.ContainsKey(rule)
                    ? _rules[rule]
                    : DefaultCacheDuration);

                var item = new CacheItem
                    {
                        Data = data,
                        Expiration = DateTime.Now.Add(expiration)
                    };

                _cache.Add(url, item);
            }
        }


        /// <summary>
        ///   Clears all cached items
        /// </summary>
        public virtual void Clear()
        {
            lock (_cacheLock)
            {
                _cache.Clear();
            }
        }

        /// <summary>
        ///   Retrieve a strongly typed cached object, if it exists, otherwise null.
        /// </summary>
        /// <typeparam name = "T"></typeparam>
        /// <param name = "url"></param>
        /// <returns></returns>
        public virtual T Get<T>(string url)
        {
            return (T)Get(url);
        }

        /// <summary>
        /// Retrieve a cached object, if it exists, otherwise null.
        /// Scavenging is done upon each get. This is not the most performant
        /// way to accomplish this but the xplatform nature of this library
        /// preclude more elegant solutions.
        /// 
        /// If the environment is constrained, you can plug in whatever cache you like.
        /// A good choice would be to leverage the asp.net cache.
        /// </summary>
        /// <param name = "url"></param>
        /// <returns></returns>
        public virtual object Get(string url)
        {

            object result = null;
            lock (_cacheLock)
            {
                Scavenge();

                if (_cache.ContainsKey(url))
                {
                    var item = _cache[url];
                    if (item.Expiration > DateTime.Now)
                    {
                        result = item.Data;
                    }
                    else
                    {
                        _cache.Remove(url);
                    }
                }
            }
            return result;
        }

        /// <summary>
        ///   Sets a cache rule keyed on the string rule.
        /// </summary>
        /// <param name = "rule"></param>
        /// <param name = "duration"></param>
        public virtual void SetRule(string rule, TimeSpan duration)
        {
            lock (_cacheLock)
            {
                _rules[rule] = duration;
            }
        }

        #endregion
        /// <summary>
        /// This method is not locked.
        /// </summary>
        protected virtual void Scavenge()
        {
            Scavenge(null);
        }

        /// <summary>
        /// This method is not locked.
        /// </summary>
        /// <param name="itemToRemove">optional item to remove along with expired items</param>
        protected virtual void Scavenge(string itemToRemove)
        {
            var toRemove = new List<string>();
            foreach (var item in _cache)
            {
                if (item.Value.Expiration < DateTime.Now || item.Key == (itemToRemove ?? ""))
                {
                    toRemove.Add(item.Key);
                }
            }

            toRemove.ForEach(s =>
                {
                    _cache.Remove(s);
                    if (Debugger.IsAttached)
                    {
                        Console.WriteLine("Removed {0} from cache", s);
                        Debug.Assert(!_cache.ContainsKey(s));
                    }
                });
        }

        #region Nested type: CacheItem

        private class CacheItem
        {
            public object Data;
            public DateTime Expiration;
        }

        #endregion
    }
}
